/* This file is part of swapper project
 *
 * Copyright (C) 2020 The Swapper Project Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 * in compliance with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */

package com.swapper.reflect;

import java.lang.reflect.*;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import java.util.Objects;

/**
 * Type util methods collection.
 * Part of the source code and inspiration came from Gson.
 */
public final class TypeHelper {
  private TypeHelper() {
    throw new UnsupportedOperationException(getClass().getSimpleName());
  }

  /**
   * Gets the raw type of the specified type.
   * An instantiable type.
   *
   * @param type the specified type.
   * @return the raw type of the specified type.
   */
  @SuppressWarnings("unchecked")
  public static <T> Class<? super T> getRawType(Type type) {
    if (type instanceof Class<?>) {
      return (Class<? super T>) type;
    } else if (type instanceof ParameterizedType) {
      ParameterizedType parameterizedType = (ParameterizedType) type;
      Type rawType = parameterizedType.getRawType();
      if (rawType instanceof Class<?>) {
        return (Class<? super T>) rawType;
      }
      throw new IllegalStateException("<rawType instanceof Class> is false!");
    } else if (type instanceof GenericArrayType) {
      Type componentType = ((GenericArrayType) type).getGenericComponentType();
      return (Class<? super T>) Array.newInstance(getRawType(componentType), 0).getClass();
    } else if (type instanceof TypeVariable) {
      return Object.class;
    } else if (type instanceof WildcardType) {
      return getRawType(((WildcardType) type).getUpperBounds()[0]);
    } else {
      throw new IllegalArgumentException("Not expected type:" + name(type));
    }
  }

  /**
   * Returns {@code true} if the specified type is primitive type.
   * Note: primitive types do not support generics.
   *
   * @param type the specified type.
   * @return {@code true} if the specified type is primitive type.
   * @see Class#isPrimitive()
   */
  public static boolean isPrimitive(Type type) {
    return type instanceof Class<?> && ((Class<?>) type).isPrimitive();
  }

  /**
   * Returns {@code true} if the specified type is an enum type.
   *
   * @param type the specified type.
   * @return {@code true} if the specified type is an enum type.
   * @see Class#isEnum()
   */
  public static boolean isEnum(Type type) {
    return type instanceof Class<?> && ((Class<?>) type).isEnum();
  }

  /**
   * Returns {@code true} if the specified type is an array type.
   *
   * @param type the specified type.
   * @return {@code true} if the specified type is an array type.
   * @see Class#isArray()
   */
  public static boolean isArray(Type type) {
    return type instanceof GenericArrayType || (type instanceof Class<?> && ((Class<?>) type).isArray());
  }

  /**
   * Returns the component type of the specified array type.
   *
   * @param type the specified array type.
   * @return the component type of the specified array type,
   * or null if the specified type is not an array.
   */
  public static Type getArrayComponentType(Type type) {
    if (type instanceof GenericArrayType) {
      return ((GenericArrayType) type).getGenericComponentType();
    } else if (type instanceof Class<?> && ((Class<?>) type).isArray()) {
      return ((Class<?>) type).getComponentType();
    }
    return null;
  }

  /**
   * Returns the element type of the specified collection type.
   *
   * @param type the specified collection type.
   * @return the element type of the specified collection type,
   * or null if the specified type is not a collection.
   */
  public static Type getCollectionElementType(Type type, Class<?> rawType) {
    if (Collection.class.isAssignableFrom(rawType)) {
      Type flatType = type instanceof WildcardType ? ((WildcardType) type).getUpperBounds()[0] : type;
      if (flatType instanceof ParameterizedType) {
        return ((ParameterizedType) flatType).getActualTypeArguments()[0];
      } else {
        return Object.class;
      }
    }
    return null;
  }

  /**
   * Returns the key and value types of the specified map type.
   *
   * @param type the specified map type.
   * @return the key and value types of the specified map type,
   * null if the specified type is not a map
   */
  public static Type[] getMapKeyAndValueTypes(Type type, Class<?> rawType) {
    if (Map.class.isAssignableFrom(rawType)) {
      if (type instanceof ParameterizedType) {
        return ((ParameterizedType) type).getActualTypeArguments();
      } else {
        return new Type[]{Object.class, Object.class};
      }
    }
    return null;
  }

  /**
   * Gets the name of the specified type.
   *
   * @param type the specified type.
   * @return the name of the specified type.
   */
  public static String name(Type type) {
    return type instanceof Class<?> ? ((Class<?>) type).getName() : String.valueOf(type);
  }

  /**
   * Returns {@code true} if the arguments are equal to each other
   * and {@code false} otherwise.
   *
   * @param a a type.
   * @param b a type to be compared with {@code a} for equality.
   * @return {@code true} if the arguments are equal to each other
   * and {@code false} otherwise.
   */
  public static boolean equals(Type a, Type b) {
    if (a == b) {
      return true;
    } else if (a instanceof Class<?>) {
      return a.equals(b);
    } else if (a instanceof ParameterizedType) {
      if (b instanceof ParameterizedType) {
        ParameterizedType pa = (ParameterizedType) Objects.requireNonNull(a);
        ParameterizedType pb = (ParameterizedType) Objects.requireNonNull(b);
        return Objects.equals(pa.getOwnerType(), pb.getOwnerType())
          && Objects.equals(pa.getRawType(), pb.getRawType())
          && Arrays.equals(pa.getActualTypeArguments(), pb.getActualTypeArguments());
      }
      return false;
    } else if (a instanceof GenericArrayType) {
      if (b instanceof GenericArrayType) {
        GenericArrayType ga = (GenericArrayType) Objects.requireNonNull(a);
        GenericArrayType gb = (GenericArrayType) Objects.requireNonNull(b);
        return equals(ga.getGenericComponentType(), gb.getGenericComponentType());
      }
      return false;
    } else if (a instanceof WildcardType) {
      if (b instanceof WildcardType) {
        WildcardType wa = (WildcardType) Objects.requireNonNull(a);
        WildcardType wb = (WildcardType) Objects.requireNonNull(b);
        return Arrays.equals(wa.getUpperBounds(), wb.getUpperBounds())
          && Arrays.equals(wa.getLowerBounds(), wb.getLowerBounds());
      }
      return false;
    } else if (a instanceof TypeVariable) {
      if (b instanceof TypeVariable) {
        TypeVariable<?> va = (TypeVariable<?>) Objects.requireNonNull(a);
        TypeVariable<?> vb = (TypeVariable<?>) Objects.requireNonNull(b);
        return Objects.equals(va.getGenericDeclaration(), vb.getGenericDeclaration())
          && Objects.equals(va.getName(), vb.getName());
      }
      return false;
    } else {
      return false;
    }
  }
}
